#include <stdio.h>
#include <stdlib.h>
#include <readline/history.h>
#include <readline/readline.h>

#include "async.h"
#include "redobject.h"
#include "redintobject.h"
#include "redmapobject.h"
#include "redboolobject.h"
#include "redfileobject.h"
#include "rednullobject.h"
#include "redarrayobject.h"
#include "redfloatobject.h"
#include "redsuperobject.h"
#include "redtupleobject.h"
#include "redmoduleobject.h"
#include "redstringobject.h"
#include "redcfunctionobject.h"
#include "redexceptionobject.h"

static char *lastline;
static RedObject *consts;
static RedObject *modules;

static RedObject *builtin_exit(void)
{
	RedException_Throw(&RedExc_SystemExit);
	return NULL;
}

static RedObject *builtin_eval(RedObject *script)
{
	if (!RedString_Exact(script))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "Script must be string");
		return NULL;
	}

	RedObject *name = RedString_FromCString("__eval__");
	RedObject *code = RedModule_Compile("<stdin>", name, script, consts);

	if (!code)
	{
		Red_UNREF(name);
		return NULL;
	}

	RedObject *module = RedModule_Eval(code);

	Red_UNREF(code);
	Red_UNREF(name);
	return module;
}

static RedObject *builtin_float(RedObject *value)
{
	if (RedString_Exact(value))
		return RedFloat_FromString(value);

	RedException_ThrowFormat(&RedExc_TypeError, "Can only convert strings to float");
	return NULL;
}

static RedObject *builtin_input(RedObject *prompt)
{
	if (!RedString_Exact(prompt))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "Input prompt must be string");
		return NULL;
	}

	char *line = readline(RedString_Buffer(prompt));

	if (!line)
	{
		RedException_ThrowFormat(&RedExc_IOError, "EOF when readline");
		return NULL;
	}

	if (!lastline || strcmp(line, lastline))
		add_history(line);

	free(lastline);
	return RedString_FromCString((lastline = line));
}

static RedObject *builtin_import(RedObject *fname)
{
	if (!RedString_Exact(fname))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "Module name must be string");
		return NULL;
	}

	RedObject *module = RedMap_GetString(modules, fname);

	if (module)
	{
		Red_REF(module);
		return module;
	}

	module = RedModule_Import(RedString_Buffer(fname), consts);

	if (!module)
		return NULL;

	RedMap_PutString(modules, fname, module);
	return module;
}

static RedObject *builtin_int(RedObject *args)
{
	switch (RedTuple_Length(args))
	{
		case 1:
		{
			RedObject *value = RedTuple_Items(args)[0];

			if (!RedString_Exact(value))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "Can only convert strings to int");
				return NULL;
			}

			return RedInt_FromString(value, 0);
		}

		case 2:
		{
			RedObject *value = RedTuple_Items(args)[0];
			RedObject *vbase = RedTuple_Items(args)[1];

			if (!RedInt_Exact(vbase))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "Number base must be integers");
				return NULL;
			}

			if (!RedString_Exact(value))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "Can only convert strings to int");
				return NULL;
			}

			return RedInt_FromString(value, (int)RedInt_Value(vbase));
		}

		default:
		{
			RedException_ThrowFormat(&RedExc_ArgumentError, "int() takes 1 ~ 2 arguments, but %d given", RedTuple_Length(args));
			return NULL;
		}
	}
}

static RedObject *builtin_open(RedObject *args)
{
	switch (RedTuple_Length(args))
	{
		case 1:
		{
			RedObject *fname = RedTuple_Items(args)[0];

			if (!RedString_Exact(fname))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "File name must be string");
				return NULL;
			}

			return RedFile_Open(RedString_Buffer(fname), "r");
		}

		case 2:
		{
			RedObject *fname = RedTuple_Items(args)[0];
			RedObject *fmode = RedTuple_Items(args)[1];

			if (!RedString_Exact(fname))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "File name must be string");
				return NULL;
			}

			if (!RedString_Exact(fmode))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "Open mode must be string");
				return NULL;
			}

			return RedFile_Open(RedString_Buffer(fname), RedString_Buffer(fmode));
		}

		default:
		{
			RedException_ThrowFormat(&RedExc_ArgumentError, "open() takes 1 ~ 2 arguments, but %d given", RedTuple_Length(args));
			return NULL;
		}
	}
}

static RedObject *builtin_print(RedObject *args)
{
	for (int i = 0; i < RedTuple_Length(args); i++)
		if (!RedObject_Print(RedTuple_Items(args)[i]))
			return NULL;
		else if (i < RedTuple_Length(args) - 1)
			printf(" ");

	Red_Return_Null();
}

static RedObject *builtin_println(RedObject *args)
{
	for (int i = 0; i < RedTuple_Length(args); i++)
		if (!RedObject_Print(RedTuple_Items(args)[i]))
			return NULL;
		else if (i < RedTuple_Length(args) - 1)
			printf(" ");

	printf("\n");
	Red_Return_Null();
}

static inline void add_const(RedObject *globals, const char *key, RedObject *value)
{
	RedObject *name = RedString_FromCString(key);

	RedMap_PutString(globals, name, value);
	Red_UNREF(name);
}

static inline void add_function(RedObject *globals, const char *key, RedObject *function)
{
	add_const(globals, key, function);
	Red_UNREF(function);
}

int main(int argc, char **argv)
{
	if (argc < 2)
	{
		fprintf(stderr, "RedScript Compiler v0.1a\n");
		fprintf(stderr, "usage: ./RedScriptCompiler <script>\n");
		return 1;
	}

	if (!Red_Init(argc, argv))
		return 1;

	consts = RedMap_CreateStringKey();
	modules = RedMap_CreateStringKey();

	/* built-in constants */
	add_const(consts, "null"	, Red_Null);
	add_const(consts, "true"	, Red_True);
	add_const(consts, "false"	, Red_False);

	/* built-in types */
	add_const(consts, "map"		, (RedObject *)&RedType_Map);
	add_const(consts, "type"	, (RedObject *)&RedType_Type);
	add_const(consts, "array"	, (RedObject *)&RedType_Array);
	add_const(consts, "super"	, (RedObject *)&RedType_Super);
	add_const(consts, "tuple"	, (RedObject *)&RedType_Tuple);
	add_const(consts, "object"	, (RedObject *)&RedType_Object);

	/* built-in functions */
	add_function(consts, "dir"		, RedCFunction_WrapUnary(RedObject_Dir));
	add_function(consts, "len"		, RedCFunction_WrapUnary(RedObject_Len));
	add_function(consts, "str"		, RedCFunction_WrapUnary(RedObject_Str));
	add_function(consts, "repr"		, RedCFunction_WrapUnary(RedObject_Repr));
	add_function(consts, "delattr"	, RedCFunction_WrapBinary(RedObject_DelAttr));
	add_function(consts, "getattr"	, RedCFunction_WrapBinary(RedObject_GetAttr));
	add_function(consts, "setattr"	, RedCFunction_WrapTernary(RedObject_SetAttr));

	add_function(consts, "eval"		, RedCFunction_WrapUnary(builtin_eval));
	add_function(consts, "input"	, RedCFunction_WrapUnary(builtin_input));
	add_function(consts, "float"	, RedCFunction_WrapUnary(builtin_float));
	add_function(consts, "import"	, RedCFunction_WrapUnary(builtin_import));
	add_function(consts, "exit"		, RedCFunction_WrapNullary(builtin_exit));

	add_function(consts, "int"		, RedCFunction_WrapVarFunc(builtin_int));
	add_function(consts, "open"		, RedCFunction_WrapVarFunc(builtin_open));
	add_function(consts, "print"	, RedCFunction_WrapVarFunc(builtin_print));
	add_function(consts, "println"	, RedCFunction_WrapVarFunc(builtin_println));

	/* built-in base exception */
	add_const(consts, "Exception"				, (RedObject *)&RedExc_Exception);

	/* built-in exception types */
	add_const(consts, "IOError"					, (RedObject *)&RedExc_IOError);
	add_const(consts, "OSError"					, (RedObject *)&RedExc_OSError);
	add_const(consts, "NameError"				, (RedObject *)&RedExc_NameError);
	add_const(consts, "TypeError"				, (RedObject *)&RedExc_TypeError);
	add_const(consts, "IndexError"				, (RedObject *)&RedExc_IndexError);
	add_const(consts, "ValueError"				, (RedObject *)&RedExc_ValueError);
	add_const(consts, "FormatError"				, (RedObject *)&RedExc_FormatError);
	add_const(consts, "ImportError"				, (RedObject *)&RedExc_ImportError);
	add_const(consts, "MemoryError"				, (RedObject *)&RedExc_MemoryError);
	add_const(consts, "CompileError"			, (RedObject *)&RedExc_CompileError);
	add_const(consts, "RuntimeError"			, (RedObject *)&RedExc_RuntimeError);
	add_const(consts, "ArgumentError"			, (RedObject *)&RedExc_ArgumentError);
	add_const(consts, "InternalError"			, (RedObject *)&RedExc_InternalError);
	add_const(consts, "AttributeError"			, (RedObject *)&RedExc_AttributeError);
	add_const(consts, "FloatPointError"			, (RedObject *)&RedExc_FloatPointError);
	add_const(consts, "UnhashableError"			, (RedObject *)&RedExc_UnhashableError);
	add_const(consts, "NotCallableError"		, (RedObject *)&RedExc_NotCallableError);
	add_const(consts, "NotPrintableError"		, (RedObject *)&RedExc_NotPrintableError);
	add_const(consts, "RecursiveTooDepthError"	, (RedObject *)&RedExc_RecursionTooDepthError);

	RedObject *module = RedModule_Import(argv[1], consts);
	RedObject *exception = RedException_Exception();

	if (exception && !RedType_IsInstance(exception, &RedExc_SystemExit))
		RedObject_Print(exception);

	Red_UNREF(module);
	Red_UNREF(consts);
	Red_UNREF(modules);

	Red_Cleanup();
	free(lastline);
	return 0;
}
